diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 5f4d88c57e9b61314c9b4ff7925ab289f20cd23b..4d774123ef1a488b24be4d10bbef1c967901ed75 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -10,7 +10,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
 
     push_frontend_feature_flag(:prometheus_computed_alerts)
   end
-  before_action :authorize_read_environment!
+  before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect]
   before_action :authorize_create_environment!, only: [:new, :create]
   before_action :authorize_stop_environment!, only: [:stop]
   before_action :authorize_update_environment!, only: [:edit, :update, :cancel_auto_stop]
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d743ea6aeea9ee774ee56b38357517735e373ec6..79546212bccc829ecdbd485a6f70b017c33b740e 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -410,7 +410,7 @@ def get_project_nav_tabs(project, current_user)
       nav_tabs << :pipelines
     end
 
-    if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project)
+    if can_view_operations_tab?(current_user, project)
       nav_tabs << :operations
     end
 
@@ -438,22 +438,29 @@ def external_nav_tabs(project)
 
   def tab_ability_map
     {
-      environments:     :read_environment,
-      milestones:       :read_milestone,
-      snippets:         :read_snippet,
-      settings:         :admin_project,
-      builds:           :read_build,
-      clusters:         :read_cluster,
-      serverless:       :read_cluster,
-      error_tracking:   :read_sentry_issue,
-      alert_management: :read_alert_management_alert,
-      labels:           :read_label,
-      issues:           :read_issue,
-      project_members:  :read_project_member,
-      wiki:             :read_wiki
+      environments:       :read_environment,
+      metrics_dashboards: :metrics_dashboard,
+      milestones:         :read_milestone,
+      snippets:           :read_snippet,
+      settings:           :admin_project,
+      builds:             :read_build,
+      clusters:           :read_cluster,
+      serverless:         :read_cluster,
+      error_tracking:     :read_sentry_issue,
+      alert_management:   :read_alert_management_alert,
+      labels:             :read_label,
+      issues:             :read_issue,
+      project_members:    :read_project_member,
+      wiki:               :read_wiki
     }
   end
 
+  def can_view_operations_tab?(current_user, project)
+    [:read_environment, :read_cluster, :metrics_dashboard].any? do |ability|
+      can?(current_user, ability, project)
+    end
+  end
+
   def search_tab_ability_map
     @search_tab_ability_map ||= tab_ability_map.merge(
       blobs:          :download_code,
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a24c0471d6ced6d20cdf8edbf48fcbfd67c06bca..44de17121deb854ffba320d4968650de5e9f784e 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -278,7 +278,6 @@ class ProjectPolicy < BasePolicy
 
   rule { can?(:metrics_dashboard) }.policy do
     enable :read_prometheus
-    enable :read_environment
     enable :read_deployment
   end
 
@@ -429,27 +428,11 @@ class ProjectPolicy < BasePolicy
   rule { builds_disabled | repository_disabled }.policy do
     prevent(*create_read_update_admin_destroy(:build))
     prevent(*create_read_update_admin_destroy(:pipeline_schedule))
+    prevent(*create_read_update_admin_destroy(:environment))
     prevent(*create_read_update_admin_destroy(:cluster))
     prevent(*create_read_update_admin_destroy(:deployment))
   end
 
-  # Enabling `read_environment` specifically for the condition of `metrics_dashboard_allowed` is
-  # necessary due to the route for metrics dashboard requiring an environment id.
-  # This will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/213833 when
-  # environments and metrics are decoupled and these rules will be removed.
-
-  rule { (builds_disabled | repository_disabled) & ~metrics_dashboard_allowed}.policy do
-    prevent(*create_read_update_admin_destroy(:environment))
-  end
-
-  rule { (builds_disabled | repository_disabled) & metrics_dashboard_allowed}.policy do
-    prevent :create_environment
-    prevent :update_environment
-    prevent :admin_environment
-    prevent :destroy_environment
-    enable :read_environment
-  end
-
   # There's two separate cases when builds_disabled is true:
   # 1. When internal CI is disabled - builds_disabled && internal_builds_disabled
   #   - We do not prevent the user from accessing Pipelines to allow them to access external CI
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index a67860e8e2eb3128c61ea3c951ca90f93a4f8f0b..c738493507f493633dc5c9d2c0977dfb4bd68b6b 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -216,7 +216,7 @@
                   =  _('Operations')
             %li.divider.fly-out-top-item
 
-            - if project_nav_tab? :environments
+            - if project_nav_tab? :metrics_dashboards
               = nav_link(controller: :environments, action: [:metrics, :metrics_redirect]) do
                 = link_to metrics_project_environments_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
                   %span
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 56fff2771ec92bf69d82cfec01d426de1d5f2eb8..cd4896ede43281da7663850c0d5152c4077a7f91 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -354,6 +354,19 @@
       expect(response).to redirect_to(environment_metrics_path(environment))
     end
 
+    context 'with anonymous user and public dashboard visibility' do
+      let(:project) { create(:project, :public) }
+      let(:user) { create(:user) }
+
+      it 'redirects successfully' do
+        project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+
+        get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
+
+        expect(response).to redirect_to(environment_metrics_path(environment))
+      end
+    end
+
     context 'when there are no environments' do
       let(:environment) { }
 
@@ -422,6 +435,19 @@
         get :metrics, params: environment_params
       end
     end
+
+    context 'with anonymous user and public dashboard visibility' do
+      let(:project) { create(:project, :public) }
+      let(:user) { create(:user) }
+
+      it 'returns success' do
+        project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+
+        get :metrics, params: environment_params
+
+        expect(response).to have_gitlab_http_status(:ok)
+      end
+    end
   end
 
   describe 'GET #additional_metrics' do
@@ -497,6 +523,26 @@
         get :metrics, params: environment_params
       end
     end
+
+    context 'with anonymous user and public dashboard visibility' do
+      let(:project) { create(:project, :public) }
+      let(:user) { create(:user) }
+
+      it 'does not fail' do
+        allow(environment)
+          .to receive(:additional_metrics)
+          .and_return({
+            success: true,
+            data: {},
+            last_update: 42
+          })
+        project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+
+        additional_metrics(window_params)
+
+        expect(response).to have_gitlab_http_status(:ok)
+      end
+    end
   end
 
   describe 'GET #metrics_dashboard' do
@@ -673,6 +719,17 @@
     it_behaves_like 'dashboard can be specified'
     it_behaves_like 'dashboard can be embedded'
 
+    context 'with anonymous user and public dashboard visibility' do
+      let(:project) { create(:project, :public) }
+      let(:user) { create(:user) }
+
+      before do
+        project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+      end
+
+      it_behaves_like 'the default dashboard'
+    end
+
     context 'permissions' do
       before do
         allow(controller).to receive(:can?).and_return true
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 189ab1a8354f8787f8e97404271ef99864e9a986..27dc8707bd8a9d9bd40436921f406c1d27ca529b 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -5,6 +5,9 @@
 describe ProjectsHelper do
   include ProjectForksHelper
 
+  let_it_be(:project) { create(:project) }
+  let_it_be(:user) { create(:user) }
+
   describe '#project_incident_management_setting' do
     let(:project) { create(:project) }
 
@@ -500,6 +503,23 @@
     end
   end
 
+  describe '#can_view_operations_tab?' do
+    before do
+      allow(helper).to receive(:current_user).and_return(user)
+    end
+
+    subject { helper.send(:can_view_operations_tab?, user, project) }
+
+    [:read_environment, :read_cluster, :metrics_dashboard].each do |ability|
+      it 'includes operations tab' do
+        allow(helper).to receive(:can?).and_return(false)
+        allow(helper).to receive(:can?).with(user, ability, project).and_return(true)
+
+        is_expected.to be(true)
+      end
+    end
+  end
+
   describe '#show_projects' do
     let(:projects) do
       create(:project)
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 09d54eb9df63d3a4ae82b3370f53b441beda1726..7c98d8cc856d55db6f8d2afacf59709ecb37cba2 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -218,41 +218,16 @@ def set_access_level(access_level)
         project.project_feature.update(builds_access_level: ProjectFeature::DISABLED)
       end
 
-      context 'without metrics_dashboard_allowed' do
-        before do
-          project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::DISABLED)
-        end
-
-        it 'disallows all permissions except pipeline when the feature is disabled' do
-          builds_permissions = [
-            :create_build, :read_build, :update_build, :admin_build, :destroy_build,
-            :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
-            :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
-            :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
-            :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
-          ]
-
-          expect_disallowed(*builds_permissions)
-        end
-      end
-
-      context 'with metrics_dashboard_allowed' do
-        before do
-          project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::ENABLED)
-        end
+      it 'disallows all permissions except pipeline when the feature is disabled' do
+        builds_permissions = [
+          :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+          :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
+          :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
+          :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+          :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
+        ]
 
-        it 'disallows all permissions except pipeline and read_environment when the feature is disabled' do
-          builds_permissions = [
-            :create_build, :read_build, :update_build, :admin_build, :destroy_build,
-            :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
-            :create_environment, :update_environment, :admin_environment, :destroy_environment,
-            :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
-            :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
-          ]
-
-          expect_disallowed(*builds_permissions)
-          expect_allowed(:read_environment)
-        end
+        expect_disallowed(*builds_permissions)
       end
     end
 
@@ -277,49 +252,20 @@ def set_access_level(access_level)
   context 'repository feature' do
     subject { described_class.new(owner, project) }
 
-    before do
+    it 'disallows all permissions when the feature is disabled' do
       project.project_feature.update(repository_access_level: ProjectFeature::DISABLED)
-    end
-
-    context 'without metrics_dashboard_allowed' do
-      before do
-        project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::DISABLED)
-      end
 
-      it 'disallows all permissions when the feature is disabled' do
-        repository_permissions = [
-          :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
-          :create_build, :read_build, :update_build, :admin_build, :destroy_build,
-          :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
-          :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
-          :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
-          :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment,
-          :destroy_release
-        ]
-
-        expect_disallowed(*repository_permissions)
-      end
-    end
-
-    context 'with metrics_dashboard_allowed' do
-      before do
-        project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::ENABLED)
-      end
-
-      it 'disallows all permissions when the feature is disabled' do
-        repository_permissions = [
-          :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
-          :create_build, :read_build, :update_build, :admin_build, :destroy_build,
-          :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
-          :create_environment, :update_environment, :admin_environment, :destroy_environment,
-          :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
-          :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment,
-          :destroy_release
-        ]
-
-        expect_disallowed(*repository_permissions)
-        expect_allowed(:read_environment)
-      end
+      repository_permissions = [
+        :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
+        :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+        :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
+        :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
+        :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
+        :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment,
+        :destroy_release
+      ]
+
+      expect_disallowed(*repository_permissions)
     end
   end