diff --git a/Gemfile b/Gemfile
index 6dc10459974154e2d4f1c4904dc5c32ec3c25e27..796dcb66b7fd7a4c73363d265c75937ea2d6879d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -740,3 +740,5 @@ gem 'gitlab-sdk', '~> 0.3.0', feature_category: :application_instrumentation
 gem 'openbao_client', path: 'gems/openbao_client' # rubocop:todo Gemfile/MissingFeatureCategory
 
 gem 'paper_trail', '~> 15.0' # rubocop:todo Gemfile/MissingFeatureCategory
+
+gem "i18n_data", "~> 0.13.1", feature_category: :system_access
diff --git a/Gemfile.lock b/Gemfile.lock
index ab893542da251845e0a94469dae0b461a4c52e88..a0009c4ca5b3140154b45533bc3c07ccf037e49d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2130,6 +2130,7 @@ DEPENDENCIES
   html-pipeline (~> 2.14.3)
   html2text
   httparty (~> 0.21.0)
+  i18n_data (~> 0.13.1)
   icalendar (~> 2.10.1)
   influxdb-client (~> 3.1)
   invisible_captcha (~> 2.1.0)
diff --git a/Gemfile.next.lock b/Gemfile.next.lock
index d4492ce46a9ff04a2b361bc1451f55860b6b90f3..e9897a21151ef581cf7dd0e1f03c3bf6fa7f01e9 100644
--- a/Gemfile.next.lock
+++ b/Gemfile.next.lock
@@ -2157,6 +2157,7 @@ DEPENDENCIES
   html-pipeline (~> 2.14.3)
   html2text
   httparty (~> 0.21.0)
+  i18n_data (~> 0.13.1)
   icalendar (~> 2.10.1)
   influxdb-client (~> 3.1)
   invisible_captcha (~> 2.1.0)
diff --git a/app/controllers/concerns/known_sign_in.rb b/app/controllers/concerns/known_sign_in.rb
index 997f26fa95906781cc6feb85a24b4f21dc469e53..6bbdde19f232618a425a7b79ef071647ac6e0a8d 100644
--- a/app/controllers/concerns/known_sign_in.rb
+++ b/app/controllers/concerns/known_sign_in.rb
@@ -46,6 +46,12 @@ def known_ip_addresses
   end
 
   def notify_user
-    current_user.notification_service.unknown_sign_in(current_user, request.remote_ip, current_user.current_sign_in_at)
+    request_info = Gitlab::Auth::VisitorLocation.new(request)
+    current_user.notification_service.unknown_sign_in(
+      current_user,
+      request.remote_ip,
+      current_user.current_sign_in_at,
+      request_info
+    )
   end
 end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 8a0d1ea2638b6ffa1b9a306b39cb10ea1972850d..9f8baf8502fba191a9bb0858c645163810578125 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -147,9 +147,11 @@ def ssh_key_expiring_soon_email(user, fingerprints)
       mail_with_locale(to: @user.notification_email_or_default, subject: subject(_("Your SSH key is expiring soon.")))
     end
 
-    def unknown_sign_in_email(user, ip, time)
+    def unknown_sign_in_email(user, ip, time, request_info = {})
       @user = user
       @ip = ip
+      @city = request_info[:city]
+      @country = request_info[:country]
       @time = time
       @target_url = edit_user_settings_password_url
 
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 43b357a75d5d9af1e35e34116fde32f371d5f3ec..f2e1e59d4815941bffc564251ba4fd657b4ee891 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -217,7 +217,7 @@ def remote_mirror_update_failed_email
   end
 
   def unknown_sign_in_email
-    Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message
+    Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current, country: 'Germany', city: 'Frankfurt').message
   end
 
   def two_factor_otp_attempt_failed_email
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index eeba7274d25deaaa5abdeba35c96d1fd745594db..60151466ba0e8ddabe457718d43816bbebdc2274 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -137,7 +137,7 @@ def ssh_key_expiring_soon(user, fingerprints)
 
   # Notify a user when a previously unknown IP or device is used to
   # sign in to their account
-  def unknown_sign_in(user, ip, time)
+  def unknown_sign_in(user, ip, time, request_info)
     return unless user.can?(:receive_notifications)
 
     mailer.unknown_sign_in_email(user, ip, time).deliver_later
diff --git a/app/views/notify/unknown_sign_in_email.html.haml b/app/views/notify/unknown_sign_in_email.html.haml
index 0e90b0568f0e0f55af805e396f73c37d07e7c386..b3fd312462f69e4e461ff2570dcf30e661a1d275 100644
--- a/app/views/notify/unknown_sign_in_email.html.haml
+++ b/app/views/notify/unknown_sign_in_email.html.haml
@@ -1,5 +1,5 @@
 - default_font = "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"
-- default_style = "#{default_font}font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"
+- default_style = "#{default_font}font-size:15px;line-height:1.4;color:#626168;font-weight:300;padding:14px 0;margin:0;"
 - spacer_style = "#{default_font};height:18px;font-size:18px;line-height:18px;"
 
 %tr.alert
@@ -33,6 +33,13 @@
           %td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
             %span.muted{ style: "color:#333333;text-decoration:none;" }
               = @ip
+        - if @city && @country
+          %tr
+            %td{ style: "#{default_style}border-top:1px solid #ededed;" }
+              = _('Location')
+            %td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+              %span.muted{ style: "color:#333333;text-decoration:none;" }
+                = @city + ", " + @country
         %tr
           %td{ style: "#{default_style}border-top:1px solid #ededed;" }
             = _('Time')
diff --git a/app/views/notify/unknown_sign_in_email.text.haml b/app/views/notify/unknown_sign_in_email.text.haml
index a926673a9cf65c23220f22da182a0ff478df678f..7a9087ff4a073024d43c5edfe4a7ec8cd069dc5e 100644
--- a/app/views/notify/unknown_sign_in_email.text.haml
+++ b/app/views/notify/unknown_sign_in_email.text.haml
@@ -1,6 +1,9 @@
 = _('Hi %{user_name} (%{user_username})!') % { user_name: sanitize_name(@user.name), user_username: @user.username }
 
-= _('A sign-in to your account has been made from the following IP address: %{ip}') % { ip: @ip }
+- if @city && @country
+  = _('A sign-in to your account has been made from the following IP address: %{ip} (%{city}, %{country})') % { ip: @ip, city: @city, country: @country }
+- else
+  = _('A sign-in to your account has been made from the following IP address: %{ip}') % { ip: @ip }
 
 = _('If you recently signed in and recognize the IP address, you may disregard this email.')
 = _('If you did not recently sign in, you should immediately change your password: %{password_link}.') % { password_link: help_page_url('user/profile/user_passwords.md', anchor: 'change-your-password') }
diff --git a/lib/gitlab/auth/visitor_location.rb b/lib/gitlab/auth/visitor_location.rb
new file mode 100644
index 0000000000000000000000000000000000000000..27d8174301abbe12057c575d2d9b3ae7641e8327
--- /dev/null
+++ b/lib/gitlab/auth/visitor_location.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# Takes in an incoming request and extracts contextual information such as location.
+#
+# Information about country and city of the request is taken from its headers set by Cloudflare WAF.
+# See: https://developers.cloudflare.com/rules/transform/managed-transforms/reference/#add-visitor-location-headers
+#
+module Gitlab
+  module Auth
+    class VisitorLocation
+      attr_reader :request
+
+      HEADERS = {
+        country: 'Cf-Ipcountry',
+        city: 'Cf-Ipcity'
+      }.freeze
+
+      # @param [ActionDispatch::Request] request
+      def initialize(request)
+        @request = request
+      end
+
+      def country
+        code = request.headers[HEADERS[:country]] # 2-letter country code, e.g. "JP" for Japan
+        # If country name is not known for local language, default to English. Or just display country code
+        I18nData.countries(I18n.locale)[code] || I18nData.countries[code] || code
+      end
+
+      def city
+        request.headers[HEADERS[:city]]
+      end
+    end
+  end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6c1c0c53d08b99c237dc897808072e3e7db9e5d1..1ba00cc1837dfd870779736c68a4e56f2f66d7a2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2124,6 +2124,9 @@ msgstr ""
 msgid "A sign-in to your account has been made from the following IP address: %{ip}"
 msgstr ""
 
+msgid "A sign-in to your account has been made from the following IP address: %{ip} (%{city}, %{country})"
+msgstr ""
+
 msgid "A template for starting a new TYPO3 project"
 msgstr ""
 
diff --git a/spec/lib/gitlab/auth/visitor_location_spec.rb b/spec/lib/gitlab/auth/visitor_location_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f24e1888f49e55c5799bd991ad155ca9806bb522
--- /dev/null
+++ b/spec/lib/gitlab/auth/visitor_location_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Auth::VisitorLocation, feature_category: :system_access do
+  let(:country_code) { 'DE' }
+  let(:country) { 'Germany' }
+  let(:city) { 'Frankfurt' }
+  let(:headers) { { "Cf-Ipcountry" => country_code, "Cf-Ipcity" => city } }
+  let(:request) { instance_double(ActionDispatch::Request, headers: headers) }
+
+  subject(:request_info) { described_class.new(request) }
+
+  it 'returns country and city' do
+    expect(request_info.country).to eq(country)
+    expect(request_info.city).to eq(city)
+  end
+
+  context 'when country code not recognized' do
+    let(:country_code) { 'UNKNOWN' }
+
+    it 'returns country code' do
+      expect(request_info.country).to eq(country_code)
+    end
+  end
+
+  context 'when locale is not default' do
+    before do
+      I18n.locale = :de
+    end
+
+    it 'returns localized country name' do
+      expect(request_info.country).to eq('Deutschland')
+    end
+  end
+
+  context 'when location headers are not set' do
+    let(:headers) { {} }
+
+    it 'cannot determine country and city' do
+      expect(request_info.country).to be_nil
+      expect(request_info.city).to be_nil
+    end
+  end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index e7e23a0d2753bccd04a0f44f4e8c39d659be3ddf..357ee7e02768b209d64bcacd3b0d20fffaa182be 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -443,10 +443,12 @@
   end
 
   describe 'user unknown sign in email' do
-    let_it_be(:user) { create(:user) }
-    let_it_be(:ip) { '169.0.0.1' }
-    let_it_be(:current_time) { Time.current }
-    let_it_be(:email) { Notify.unknown_sign_in_email(user, ip, current_time) }
+    let(:user) { create(:user) }
+    let(:ip) { '169.0.0.1' }
+    let(:current_time) { Time.current }
+    let(:country) { 'Germany' }
+    let(:city) { 'Frankfurt' }
+    let(:email) { Notify.unknown_sign_in_email(user, ip, current_time, country: country, city: city) }
 
     subject { email }
 
@@ -487,6 +489,19 @@
       is_expected.to have_body_text help_page_url('user/profile/account/two_factor_authentication.md')
     end
 
+    it 'shows location information' do
+      is_expected.to have_body_text _('Location')
+      is_expected.to have_body_text country
+      is_expected.to have_body_text city
+    end
+
+    context 'when no location information was given' do
+      let(:country) { nil }
+      let(:city) { nil }
+
+      it { is_expected.not_to have_body_text _('Location') }
+    end
+
     context 'when two factor authentication is enabled' do
       let(:user) { create(:user, :two_factor) }
 
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 1b47c73e9d3da54ff250afec00260fdfcfdede33..5118dc4f682497ad9e9aee1d6d118a4d1d23addf 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -652,11 +652,14 @@
   end
 
   describe '#unknown_sign_in' do
-    let_it_be(:user) { create(:user) }
-    let_it_be(:ip) { '127.0.0.1' }
-    let_it_be(:time) { Time.current }
+    let(:user) { create(:user) }
+    let(:ip) { '127.0.0.1' }
+    let(:country) { 'Germany' }
+    let(:city) { 'Frankfurt' }
+    let(:request_info) { Struct.new(:country, :city).new(country, city) }
+    let(:time) { Time.current }
 
-    subject { notification.unknown_sign_in(user, ip, time) }
+    subject { notification.unknown_sign_in(user, ip, time, request_info) }
 
     it 'sends email to the user' do
       expect { subject }.to have_enqueued_email(user, ip, time, mail: 'unknown_sign_in_email')