diff --git a/doc/api/projects.md b/doc/api/projects.md
index 75eea394a4013f53ae898dd03258768d0c66236b..32b26f3272649ad9dde6f37ec67c31972d12a8b2 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -2596,6 +2596,50 @@ DELETE /projects/:id/push_rule
 |-----------|----------------|------------------------|-------------|
 | `id`      | integer or string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). |
 
+## Get groups to which a user can transfer a project
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371006) in GitLab 15.4
+
+Retrieve a list of groups to which the user can transfer a project.
+
+```plaintext
+GET /projects/:id/transfer_locations
+```
+
+| Attribute   | Type           | Required               | Description |
+|-------------|----------------|------------------------|-------------|
+| `id`        | integer or string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). |
+| `search` | string | **{dotted-circle}** No  | The group names to search for. |
+
+Example request:
+
+```shell
+curl --request GET "https://gitlab.example.com/api/v4/projects/1/transfer_locations"
+```
+
+Example response:
+
+```json
+[
+  {
+    "id": 27,
+    "web_url": "https://gitlab.example.com/groups/gitlab",
+    "name": "GitLab",
+    "avatar_url": null,
+    "full_name": "GitLab",
+    "full_path": "GitLab"
+  },
+  {
+    "id": 31,
+    "web_url": "https://gitlab.example.com/groups/foobar",
+    "name": "FooBar",
+    "avatar_url": null,
+    "full_name": "FooBar",
+    "full_path": "FooBar"
+  }
+]
+```
+
 ## Transfer a project to a new namespace
 
 > The `_links.cluster_agents` attribute in the response [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/347047) in GitLab 14.10.
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6ed480518ee71ee4f38f94d3ec48bb0dada02ea7..f8dc3d45a5c8fa6425d23c3d8d7b088cd65da5b7 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -743,6 +743,22 @@ def add_import_params(params)
         end
       end
 
+      desc 'Get the namespaces to where the project can be transferred'
+      params do
+        optional :search, type: String, desc: 'Return list of namespaces matching the search criteria'
+        use :pagination
+      end
+      get ":id/transfer_locations", feature_category: :projects do
+        authorize! :change_namespace, user_project
+        args = declared_params(include_missing: false)
+        args[:permission_scope] = :transfer_projects
+
+        groups = ::Groups::UserGroupsFinder.new(current_user, current_user, args).execute
+        groups = groups.with_route
+
+        present_groups(groups)
+      end
+
       desc 'Show the storage information' do
         success Entities::ProjectRepositoryStorage
       end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 94688833d88b2fe95cee455f9ff0fac43d30d800..632e15084f13254a7459f7d4df7a22b3e212b550 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -4645,6 +4645,112 @@ def failure_message(diff)
     end
   end
 
+  describe 'GET /projects/:id/transfer_locations' do
+    let_it_be(:user) { create(:user) }
+    let_it_be(:source_group) { create(:group) }
+    let_it_be(:project) { create(:project, group: source_group) }
+
+    let(:params) { {} }
+
+    subject(:request) do
+      get api("/projects/#{project.id}/transfer_locations", user), params: params
+    end
+
+    context 'when the user has rights to transfer the project' do
+      let_it_be(:guest_group) { create(:group) }
+      let_it_be(:maintainer_group) { create(:group, name: 'maintainer group', path: 'maintainer-group') }
+      let_it_be(:owner_group) { create(:group, name: 'owner group', path: 'owner-group') }
+
+      before do
+        source_group.add_owner(user)
+        guest_group.add_guest(user)
+        maintainer_group.add_maintainer(user)
+        owner_group.add_owner(user)
+      end
+
+      it 'returns 200' do
+        request
+
+        expect(response).to have_gitlab_http_status(:ok)
+        expect(response).to include_pagination_headers
+      end
+
+      it 'includes groups where the user has permissions to transfer a project to' do
+        request
+
+        expect(project_ids_from_response).to include(maintainer_group.id, owner_group.id)
+      end
+
+      it 'does not include groups where the user doesn not have permissions to transfer a project' do
+        request
+
+        expect(project_ids_from_response).not_to include(guest_group.id)
+      end
+
+      context 'with search' do
+        let(:params) { { search: 'maintainer' } }
+
+        it 'includes groups where the user has permissions to transfer a project to' do
+          request
+
+          expect(project_ids_from_response).to contain_exactly(maintainer_group.id)
+        end
+      end
+
+      context 'group shares' do
+        let_it_be(:shared_to_owner_group) { create(:group) }
+        let_it_be(:shared_to_guest_group) { create(:group) }
+
+        before do
+          create(:group_group_link, :owner,
+                 shared_with_group: owner_group,
+                 shared_group: shared_to_owner_group
+          )
+
+          create(:group_group_link, :guest,
+                 shared_with_group: guest_group,
+                 shared_group: shared_to_guest_group
+          )
+        end
+
+        it 'only includes groups arising from group shares where the user has permission to transfer a project to' do
+          request
+
+          expect(project_ids_from_response).to include(shared_to_owner_group.id)
+          expect(project_ids_from_response).not_to include(shared_to_guest_group.id)
+        end
+
+        context 'when the feature flag `include_groups_from_group_shares_in_project_transfer_locations` is disabled' do
+          before do
+            stub_feature_flags(include_groups_from_group_shares_in_project_transfer_locations: false)
+          end
+
+          it 'does not include any groups arising from group shares' do
+            request
+
+            expect(project_ids_from_response).not_to include(shared_to_owner_group.id, shared_to_guest_group.id)
+          end
+        end
+      end
+
+      def project_ids_from_response
+        json_response.map { |project| project['id'] }
+      end
+    end
+
+    context 'when the user does not have permissions to transfer the project' do
+      before do
+        source_group.add_developer(user)
+      end
+
+      it 'returns 403' do
+        request
+
+        expect(response).to have_gitlab_http_status(:forbidden)
+      end
+    end
+  end
+
   describe 'GET /projects/:id/storage' do
     context 'when unauthenticated' do
       it 'does not return project storage data' do