diff --git a/api/cache/objectslist.go b/api/cache/objectslist.go
index e28d47b9..7eed1bd1 100644
--- a/api/cache/objectslist.go
+++ b/api/cache/objectslist.go
@@ -33,8 +33,9 @@ type (
 
 	// ObjectsListKey is a key to find a ObjectsListCache's entry.
 	ObjectsListKey struct {
-		cid    string
-		prefix string
+		cid        string
+		prefix     string
+		latestOnly bool
 	}
 )
 
@@ -103,11 +104,12 @@ func (l *ObjectsListCache) CleanCacheEntriesContainingObject(objectName string,
 	}
 }
 
-// CreateObjectsListCacheKey returns ObjectsListKey with the given CID and prefix.
-func CreateObjectsListCacheKey(cnr cid.ID, prefix string) ObjectsListKey {
+// CreateObjectsListCacheKey returns ObjectsListKey with the given CID, prefix and latestOnly flag.
+func CreateObjectsListCacheKey(cnr *cid.ID, prefix string, latestOnly bool) ObjectsListKey {
 	p := ObjectsListKey{
-		cid:    cnr.EncodeToString(),
-		prefix: prefix,
+		cid:        cnr.EncodeToString(),
+		prefix:     prefix,
+		latestOnly: latestOnly,
 	}
 
 	return p
diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go
index bd53e638..7793036f 100644
--- a/api/handler/handlers_test.go
+++ b/api/handler/handlers_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/nspcc-dev/neofs-s3-gw/api/data"
 	"github.com/nspcc-dev/neofs-s3-gw/api/layer"
 	"github.com/nspcc-dev/neofs-s3-gw/api/resolver"
+	treetest "github.com/nspcc-dev/neofs-s3-gw/internal/neofstest/tree"
 	cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
 	"github.com/nspcc-dev/neofs-sdk-go/object"
 	"github.com/nspcc-dev/neofs-sdk-go/user"
@@ -55,9 +56,10 @@ func prepareHandlerContext(t *testing.T) *handlerContext {
 	})
 
 	layerCfg := &layer.Config{
-		Caches:   layer.DefaultCachesConfigs(zap.NewExample()),
-		AnonKey:  layer.AnonymousKey{Key: key},
-		Resolver: testResolver,
+		Caches:      layer.DefaultCachesConfigs(zap.NewExample()),
+		AnonKey:     layer.AnonymousKey{Key: key},
+		Resolver:    testResolver,
+		TreeService: treetest.NewTreeService(),
 	}
 
 	h := &handler{
diff --git a/api/layer/object.go b/api/layer/object.go
index 6d828eb1..6ffe36f8 100644
--- a/api/layer/object.go
+++ b/api/layer/object.go
@@ -503,19 +503,11 @@ func (n *layer) ListObjectsV2(ctx context.Context, p *ListObjectsParamsV2) (*Lis
 }
 
 func (n *layer) listSortedObjects(ctx context.Context, p allObjectParams) ([]*data.ObjectInfo, error) {
-	versions, err := n.getAllObjectsVersions(ctx, p.Bucket, p.Prefix, p.Delimiter)
+	objects, err := n.getLatestObjectsVersions(ctx, p.Bucket, p.Prefix, p.Delimiter)
 	if err != nil {
 		return nil, err
 	}
 
-	objects := make([]*data.ObjectInfo, 0, len(versions))
-	for _, v := range versions {
-		lastVersion := v.getLast()
-		if lastVersion != nil {
-			objects = append(objects, lastVersion)
-		}
-	}
-
 	sort.Slice(objects, func(i, j int) bool {
 		return objects[i].Name < objects[j].Name
 	})
@@ -523,10 +515,49 @@ func (n *layer) listSortedObjects(ctx context.Context, p allObjectParams) ([]*da
 	return objects, nil
 }
 
+func (n *layer) getLatestObjectsVersions(ctx context.Context, bkt *data.BucketInfo, prefix, delimiter string) ([]*data.ObjectInfo, error) {
+	var err error
+
+	cacheKey := cache.CreateObjectsListCacheKey(&bkt.CID, prefix, true)
+	ids := n.listsCache.Get(cacheKey)
+
+	if ids == nil {
+		ids, err = n.treeService.GetLatestVersionsByPrefix(ctx, &bkt.CID, prefix)
+		if err != nil {
+			return nil, err
+		}
+		if err := n.listsCache.Put(cacheKey, ids); err != nil {
+			n.log.Error("couldn't cache list of objects", zap.Error(err))
+		}
+	}
+
+	objectsMap := make(map[string]*data.ObjectInfo, len(ids)) // to squash the same directories
+	for i := 0; i < len(ids); i++ {
+		obj := n.objectFromObjectsCacheOrNeoFS(ctx, bkt, ids[i])
+		if obj == nil {
+			continue
+		}
+		if oi := objectInfoFromMeta(bkt, obj, prefix, delimiter); oi != nil {
+			if isSystem(oi) {
+				continue
+			}
+
+			objectsMap[oi.Name] = oi
+		}
+	}
+
+	objects := make([]*data.ObjectInfo, 0, len(objectsMap))
+	for _, obj := range objectsMap {
+		objects = append(objects, obj)
+	}
+
+	return objects, nil
+}
+
 func (n *layer) getAllObjectsVersions(ctx context.Context, bkt *data.BucketInfo, prefix, delimiter string) (map[string]*objectVersions, error) {
 	var err error
 
-	cacheKey := cache.CreateObjectsListCacheKey(bkt.CID, prefix)
+	cacheKey := cache.CreateObjectsListCacheKey(&bkt.CID, prefix, false)
 	ids := n.listsCache.Get(cacheKey)
 
 	if ids == nil {
diff --git a/api/layer/tree_service.go b/api/layer/tree_service.go
index d933b11a..dfbec0bc 100644
--- a/api/layer/tree_service.go
+++ b/api/layer/tree_service.go
@@ -32,6 +32,7 @@ type TreeService interface {
 
 	GetVersions(ctx context.Context, cnrID *cid.ID, objectName string) ([]*NodeVersion, error)
 	GetLatestVersion(ctx context.Context, cnrID *cid.ID, objectName string) (*NodeVersion, error)
+	GetLatestVersionsByPrefix(ctx context.Context, cnrID *cid.ID, prefix string) ([]oid.ID, error)
 	GetUnversioned(ctx context.Context, cnrID *cid.ID, objectName string) (*NodeVersion, error)
 	AddVersion(ctx context.Context, cnrID *cid.ID, objectName string, newVersion *NodeVersion) error
 	RemoveVersion(ctx context.Context, cnrID *cid.ID, nodeID uint64) error
diff --git a/internal/neofs/tree.go b/internal/neofs/tree.go
index 64afedea..9e84a771 100644
--- a/internal/neofs/tree.go
+++ b/internal/neofs/tree.go
@@ -56,8 +56,12 @@ const (
 	systemTree = "system"
 
 	separator = "/"
+
+	maxGetSubTreeDepth = 10 // current limit on storage node side
 )
 
+var emptyOID oid.ID
+
 // NewTreeClient creates instance of TreeClient using provided address and create grpc connection.
 func NewTreeClient(addr string, key *keys.PrivateKey) (*TreeClient, error) {
 	conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
@@ -74,7 +78,13 @@ func NewTreeClient(addr string, key *keys.PrivateKey) (*TreeClient, error) {
 	}, nil
 }
 
-func newTreeNode(nodeInfo *tree.GetNodeByPathResponse_Info) (*TreeNode, error) {
+type NodeResponse interface {
+	GetMeta() []*tree.KeyValue
+	GetNodeId() uint64
+	GetTimestamp() uint64
+}
+
+func newTreeNode(nodeInfo NodeResponse) (*TreeNode, error) {
 	var objID oid.ID
 	meta := make(map[string]string, len(nodeInfo.GetMeta()))
 
@@ -92,7 +102,7 @@ func newTreeNode(nodeInfo *tree.GetNodeByPathResponse_Info) (*TreeNode, error) {
 	return &TreeNode{
 		ID:        nodeInfo.GetNodeId(),
 		ObjID:     objID,
-		TimeStamp: nodeInfo.Timestamp,
+		TimeStamp: nodeInfo.GetTimestamp(),
 		Meta:      meta,
 	}, nil
 }
@@ -102,7 +112,7 @@ func (n *TreeNode) Get(key string) (string, bool) {
 	return value, ok
 }
 
-func newNodeVersion(node *tree.GetNodeByPathResponse_Info) (*layer.NodeVersion, error) {
+func newNodeVersion(node NodeResponse) (*layer.NodeVersion, error) {
 	treeNode, err := newTreeNode(node)
 	if err != nil {
 		return nil, fmt.Errorf("invalid tree node: %w", err)
@@ -113,7 +123,7 @@ func newNodeVersion(node *tree.GetNodeByPathResponse_Info) (*layer.NodeVersion,
 
 	return &layer.NodeVersion{
 		BaseNodeVersion: layer.BaseNodeVersion{
-			ID:  node.NodeId,
+			ID:  treeNode.ID,
 			OID: treeNode.ObjID,
 		},
 		IsUnversioned:  isUnversioned,
@@ -242,6 +252,96 @@ func (c *TreeClient) GetLatestVersion(ctx context.Context, cnrID *cid.ID, object
 	return c.getLatestVersion(ctx, cnrID, versionTree, fileNameKV, path, meta)
 }
 
+func (c *TreeClient) GetLatestVersionsByPrefix(ctx context.Context, cnrID *cid.ID, prefix string) ([]oid.ID, error) {
+	var rootID uint64
+	path := strings.Split(prefix, separator)
+	tailPrefix := path[len(path)-1]
+
+	if len(path) > 1 {
+		meta := []string{fileNameKV}
+
+		nodes, err := c.getNodes(ctx, cnrID, versionTree, fileNameKV, path[:len(path)-1], meta, true)
+		if err != nil {
+			return nil, err
+		}
+		if len(nodes) == 0 {
+			return nil, nil
+		}
+		if len(nodes) != 1 {
+			return nil, layer.ErrNodeNotFound
+		}
+
+		rootID = nodes[0].NodeId
+	}
+
+	subTree, err := c.getSubTree(ctx, cnrID, versionTree, rootID, 1)
+	if err != nil {
+		return nil, err
+	}
+
+	var result []oid.ID
+	for _, node := range subTree {
+		if node.GetNodeId() != 0 && hasPrefix(node, tailPrefix) {
+			latestNodes, err := c.getSubTreeLatestVersions(ctx, cnrID, node.GetNodeId())
+			if err != nil {
+				return nil, err
+			}
+			result = append(result, latestNodes...)
+		}
+	}
+
+	return result, nil
+}
+
+func hasPrefix(node *tree.GetSubTreeResponse_Body, prefix string) bool {
+	for _, kv := range node.GetMeta() {
+		if kv.GetKey() == fileNameKV {
+			return strings.HasPrefix(string(kv.GetValue()), prefix)
+		}
+	}
+
+	return false
+}
+
+func (c *TreeClient) getSubTreeLatestVersions(ctx context.Context, cnrID *cid.ID, nodeID uint64) ([]oid.ID, error) {
+	subTree, err := c.getSubTree(ctx, cnrID, versionTree, nodeID, maxGetSubTreeDepth)
+	if err != nil {
+		return nil, err
+	}
+
+	latestVersions := make(map[string]*TreeNode, len(subTree))
+	for _, node := range subTree {
+		treeNode, err := newTreeNode(node)
+		if err != nil || treeNode.ObjID.Equals(emptyOID) { // invalid OID attribute
+			continue
+		}
+		fileName, ok := treeNode.Get(fileNameKV)
+		if !ok {
+			continue
+		}
+
+		key := formLatestNodeKey(node.GetParentId(), fileName)
+		latest, ok := latestVersions[key]
+		if !ok || latest.TimeStamp <= treeNode.TimeStamp { // todo also compare oid
+			latestVersions[key] = treeNode
+		}
+	}
+
+	result := make([]oid.ID, 0, len(latestVersions))
+	for _, treeNode := range latestVersions {
+		if _, ok := treeNode.Get(isDeleteMarkerKV); ok {
+			continue
+		}
+		result = append(result, treeNode.ObjID)
+	}
+
+	return result, nil
+}
+
+func formLatestNodeKey(parentID uint64, fileName string) string {
+	return strconv.FormatUint(parentID, 10) + fileName
+}
+
 func (c *TreeClient) GetSystemVersion(ctx context.Context, cnrID *cid.ID, objectName string) (*layer.BaseNodeVersion, error) {
 	meta := []string{oidKV}
 	path := strings.Split(objectName, separator)
@@ -379,11 +479,21 @@ func (c *TreeClient) getVersions(ctx context.Context, cnrID *cid.ID, treeID, fil
 }
 
 func (c *TreeClient) getParent(ctx context.Context, cnrID *cid.ID, treeID string, id uint64) (uint64, error) {
+	subTree, err := c.getSubTree(ctx, cnrID, treeID, id, 0)
+	if err != nil {
+		return 0, err
+	}
+
+	return subTree[0].GetParentId(), nil
+}
+
+func (c *TreeClient) getSubTree(ctx context.Context, cnrID *cid.ID, treeID string, rootID uint64, depth uint32) ([]*tree.GetSubTreeResponse_Body, error) {
 	request := &tree.GetSubTreeRequest{
 		Body: &tree.GetSubTreeRequest_Body{
 			ContainerId: cnrID[:],
 			TreeId:      treeID,
-			RootId:      id,
+			RootId:      rootID,
+			Depth:       depth,
 			BearerToken: getBearer(ctx),
 		},
 	}
@@ -394,28 +504,32 @@ func (c *TreeClient) getParent(ctx context.Context, cnrID *cid.ID, treeID string
 			Sign: sign,
 		}
 	}); err != nil {
-		return 0, err
+		return nil, err
 	}
 
 	cli, err := c.service.GetSubTree(ctx, request)
 	if err != nil {
-		return 0, fmt.Errorf("failed to get sub tree client: %w", err)
-	}
-
-	resp, err := cli.Recv()
-	if err != nil {
-		return 0, fmt.Errorf("failed to get sub tree: %w", err)
+		if strings.Contains(err.Error(), "not found") {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("failed to get sub tree client: %w", err)
 	}
 
+	var subtree []*tree.GetSubTreeResponse_Body
 	for {
-		if _, err = cli.Recv(); err == io.EOF {
+		resp, err := cli.Recv()
+		if err == io.EOF {
 			break
 		} else if err != nil {
-			return 0, fmt.Errorf("failed to read out sub tree stream: %w", err)
+			if strings.Contains(err.Error(), "not found") {
+				return nil, nil
+			}
+			return nil, fmt.Errorf("failed to get sub tree: %w", err)
 		}
+		subtree = append(subtree, resp.Body)
 	}
 
-	return resp.GetBody().GetParentId(), nil
+	return subtree, nil
 }
 
 func metaFromSettings(settings *data.BucketSettings) map[string]string {
@@ -474,7 +588,7 @@ func (c *TreeClient) getNodes(ctx context.Context, cnrID *cid.ID, treeID, pathAt
 
 	resp, err := c.service.GetNodeByPath(ctx, request)
 	if err != nil {
-		return nil, fmt.Errorf("failed to get node path deb: %w", err)
+		return nil, fmt.Errorf("failed to get node path: %w", err)
 	}
 
 	return resp.GetBody().GetNodes(), nil
diff --git a/internal/neofstest/tree/tree_mock.go b/internal/neofstest/tree/tree_mock.go
new file mode 100644
index 00000000..ca48479e
--- /dev/null
+++ b/internal/neofstest/tree/tree_mock.go
@@ -0,0 +1,157 @@
+package tree
+
+import (
+	"context"
+	"sort"
+
+	"github.com/nspcc-dev/neofs-s3-gw/api/data"
+	"github.com/nspcc-dev/neofs-s3-gw/api/layer"
+	cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
+	oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
+)
+
+type TreeServiceMock struct {
+	settings map[string]*data.BucketSettings
+	versions map[string]map[string][]*layer.NodeVersion
+	system   map[string]map[string]*layer.BaseNodeVersion
+}
+
+func NewTreeService() *TreeServiceMock {
+	return &TreeServiceMock{
+		settings: make(map[string]*data.BucketSettings),
+		versions: make(map[string]map[string][]*layer.NodeVersion),
+		system:   make(map[string]map[string]*layer.BaseNodeVersion),
+	}
+}
+
+func (t *TreeServiceMock) PutSettingsNode(_ context.Context, id *cid.ID, settings *data.BucketSettings) error {
+	t.settings[id.EncodeToString()] = settings
+	return nil
+}
+
+func (t *TreeServiceMock) GetSettingsNode(_ context.Context, id *cid.ID) (*data.BucketSettings, error) {
+	settings, ok := t.settings[id.EncodeToString()]
+	if !ok {
+		return nil, layer.ErrNodeNotFound
+	}
+
+	return settings, nil
+}
+
+func (t *TreeServiceMock) GetNotificationConfigurationNode(ctx context.Context, cnrID *cid.ID) (*oid.ID, error) {
+	panic("implement me")
+}
+
+func (t *TreeServiceMock) PutNotificationConfigurationNode(ctx context.Context, cnrID *cid.ID, objID *oid.ID) (*oid.ID, error) {
+	panic("implement me")
+}
+
+func (t *TreeServiceMock) GetBucketCORS(ctx context.Context, cnrID *cid.ID) (*oid.ID, error) {
+	panic("implement me")
+}
+
+func (t *TreeServiceMock) PutBucketCORS(ctx context.Context, cnrID *cid.ID, objID *oid.ID) (*oid.ID, error) {
+	panic("implement me")
+}
+
+func (t *TreeServiceMock) DeleteBucketCORS(ctx context.Context, cnrID *cid.ID) (*oid.ID, error) {
+	panic("implement me")
+}
+
+func (t *TreeServiceMock) GetVersions(ctx context.Context, cnrID *cid.ID, objectName string) ([]*layer.NodeVersion, error) {
+	panic("implement me")
+}
+
+func (t *TreeServiceMock) GetLatestVersion(ctx context.Context, cnrID *cid.ID, objectName string) (*layer.NodeVersion, error) {
+	cnrVersionsMap, ok := t.versions[cnrID.EncodeToString()]
+	if !ok {
+		return nil, layer.ErrNodeNotFound
+	}
+
+	versions, ok := cnrVersionsMap[objectName]
+	if !ok {
+		return nil, layer.ErrNodeNotFound
+	}
+
+	sort.Slice(versions, func(i, j int) bool {
+		return versions[i].ID < versions[j].ID
+	})
+
+	if len(versions) != 0 {
+		return versions[len(versions)-1], nil
+	}
+
+	return nil, layer.ErrNodeNotFound
+}
+
+func (t *TreeServiceMock) GetLatestVersionsByPrefix(ctx context.Context, cnrID *cid.ID, prefix string) ([]oid.ID, error) {
+	panic("implement me")
+}
+
+func (t *TreeServiceMock) GetUnversioned(ctx context.Context, cnrID *cid.ID, objectName string) (*layer.NodeVersion, error) {
+	panic("implement me")
+}
+
+func (t *TreeServiceMock) AddVersion(_ context.Context, cnrID *cid.ID, objectName string, newVersion *layer.NodeVersion) error {
+	cnrVersionsMap, ok := t.versions[cnrID.EncodeToString()]
+	if !ok {
+		t.versions[cnrID.EncodeToString()] = map[string][]*layer.NodeVersion{
+			objectName: {newVersion},
+		}
+		return nil
+	}
+
+	versions, ok := cnrVersionsMap[objectName]
+	if !ok {
+		cnrVersionsMap[objectName] = []*layer.NodeVersion{newVersion}
+		return nil
+	}
+
+	sort.Slice(versions, func(i, j int) bool {
+		return versions[i].ID < versions[j].ID
+	})
+
+	if len(versions) != 0 {
+		newVersion.ID = versions[len(versions)-1].ID + 1
+	}
+
+	cnrVersionsMap[objectName] = append(versions, newVersion)
+
+	return nil
+}
+
+func (t *TreeServiceMock) RemoveVersion(ctx context.Context, cnrID *cid.ID, nodeID uint64) error {
+	panic("implement me")
+}
+
+func (t *TreeServiceMock) AddSystemVersion(_ context.Context, cnrID *cid.ID, objectName string, newVersion *layer.BaseNodeVersion) error {
+	cnrSystemMap, ok := t.system[cnrID.EncodeToString()]
+	if !ok {
+		t.system[cnrID.EncodeToString()] = map[string]*layer.BaseNodeVersion{
+			objectName: newVersion,
+		}
+		return nil
+	}
+
+	cnrSystemMap[objectName] = newVersion
+
+	return nil
+}
+
+func (t *TreeServiceMock) GetSystemVersion(_ context.Context, cnrID *cid.ID, objectName string) (*layer.BaseNodeVersion, error) {
+	cnrSystemMap, ok := t.system[cnrID.EncodeToString()]
+	if !ok {
+		return nil, layer.ErrNodeNotFound
+	}
+
+	sysVersion, ok := cnrSystemMap[objectName]
+	if !ok {
+		return nil, layer.ErrNodeNotFound
+	}
+
+	return sysVersion, nil
+}
+
+func (t *TreeServiceMock) RemoveSystemVersion(ctx context.Context, cnrID *cid.ID, nodeID uint64) error {
+	panic("implement me")
+}