diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/UriHelper.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/UriHelper.ts
index 53442dae6217531bd50984a1d92167531978ad75..8e77f1a6b5d0e56baa5ef74ec5121109476e9d85 100644
--- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/UriHelper.ts
+++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/UriHelper.ts
@@ -1,6 +1,6 @@
 import { registerFunction } from '../Interop/RegisteredFunction';
 import { platform } from '../Environment';
-import { MethodHandle } from '../Platform/Platform';
+import { MethodHandle, System_String } from '../Platform/Platform';
 const registeredFunctionPrefix = 'Microsoft.AspNetCore.Blazor.Browser.Services.BrowserUriHelper';
 let notifyLocationChangedMethod: MethodHandle;
 let hasRegisteredEventListeners = false;
@@ -24,8 +24,7 @@ registerFunction(`${registeredFunctionPrefix}.enableNavigationInteception`, () =
       const href = anchorTarget.getAttribute('href');
       if (isWithinBaseUriSpace(toAbsoluteUri(href))) {
         event.preventDefault();
-        history.pushState(null, /* ignored title */ '', href);
-        handleInternalNavigation();
+        performInternalNavigation(href);
       }
     }
   });
@@ -33,6 +32,20 @@ registerFunction(`${registeredFunctionPrefix}.enableNavigationInteception`, () =
   window.addEventListener('popstate', handleInternalNavigation);
 });
 
+registerFunction(`${registeredFunctionPrefix}.navigateTo`, (uriDotNetString: System_String) => {
+  const href = platform.toJavaScriptString(uriDotNetString);
+  if (isWithinBaseUriSpace(toAbsoluteUri(href))) {
+    performInternalNavigation(href);
+  } else {
+    location.href = href;
+  }
+});
+
+function performInternalNavigation(href: string) {
+  history.pushState(null, /* ignored title */ '', href);
+  handleInternalNavigation();
+}
+
 function handleInternalNavigation() {
   if (!notifyLocationChangedMethod) {
     notifyLocationChangedMethod = platform.findMethod(
diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserUriHelper.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserUriHelper.cs
index 28cf8e497fbf19a8dcdb01747b7189942beb543b..171679febb9a1ac4c22f856e220ac7a188fe2657 100644
--- a/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserUriHelper.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Browser/Services/BrowserUriHelper.cs
@@ -104,6 +104,17 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services
             throw new ArgumentException($"The URI '{absoluteUri}' is not contained by the base URI '{baseUriPrefix}'.");
         }
 
+        /// <inheritdoc />
+        public void NavigateTo(string uri)
+        {
+            if (uri == null)
+            {
+                throw new ArgumentNullException(nameof(uri));
+            }
+
+            RegisteredFunction.InvokeUnmarshalled<object>($"{_functionPrefix}.navigateTo", uri);
+        }
+
         private static void EnsureBaseUriPopulated()
         {
             // The <base href> is fixed for the lifetime of the page, so just cache it
diff --git a/src/Microsoft.AspNetCore.Blazor/Services/IUriHelper.cs b/src/Microsoft.AspNetCore.Blazor/Services/IUriHelper.cs
index 12e8fd0dade16923eaf1936e31cd12361ac18795..472b7956f56088d0b156d906e7228ab78043d6c1 100644
--- a/src/Microsoft.AspNetCore.Blazor/Services/IUriHelper.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Services/IUriHelper.cs
@@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Blazor.Services
         /// <summary>
         /// Gets the current absolute URI.
         /// </summary>
-        /// <returns>The browser's current absolute URI.</returns>
+        /// <returns>The current absolute URI.</returns>
         string GetAbsoluteUri();
 
         /// <summary>
@@ -44,5 +44,12 @@ namespace Microsoft.AspNetCore.Blazor.Services
         /// <param name="absoluteUri">An absolute URI that is within the space of the base URI prefix.</param>
         /// <returns>A relative URI path.</returns>
         string ToBaseRelativePath(string baseUriPrefix, string locationAbsolute);
+
+        /// <summary>
+        /// Navigates to the specified URI.
+        /// </summary>
+        /// <param name="uri">The destination URI. This can be absolute, or relative to the base URI
+        /// prefix (as returned by <see cref="GetBaseUriPrefix"/>).</param>
+        void NavigateTo(string uri);
     }
 }
diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs
index 8970de915cd0572ccd55b88420cfa8c5c5a5dcfc..9b6cfd157685b483ecfb51b1d151ce031f316f5e 100644
--- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/RoutingTest.cs
@@ -110,6 +110,16 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
             Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text);
         }
 
+        [Fact]
+        public void CanNavigateProgrammatically()
+        {
+            SetUrlViaPushState($"{ServerPathBase}/RouterTest/");
+
+            var app = MountTestComponent<TestRouter>();
+            app.FindElement(By.TagName("button")).Click();
+            Assert.Equal("This is another page.", app.FindElement(By.Id("test-info")).Text);
+        }
+
         public void Dispose()
         {
             // Clear any existing state
diff --git a/test/testapps/BasicTestApp/RouterTest/Links.cshtml b/test/testapps/BasicTestApp/RouterTest/Links.cshtml
index e7bbca5d1205900da6c831572e6e0c7f65947d91..9490da172537e9b68ffc813d0f6c5d86f0bb3275 100644
--- a/test/testapps/BasicTestApp/RouterTest/Links.cshtml
+++ b/test/testapps/BasicTestApp/RouterTest/Links.cshtml
@@ -1,4 +1,6 @@
-<ul>
+@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper uriHelper
+
+<ul>
     <li><a href="/subdir/RouterTest/">Default</a></li>
     <li><a href="/subdir/RouterTest/?abc=123">Default with query</a></li>
     <li><a href="/subdir/RouterTest/#blah">Default with hash</a></li>
@@ -7,3 +9,7 @@
     <li><a href="/subdir/RouterTest/Other?abc=123">Other with query</a></li>
     <li><a href="/subdir/RouterTest/Other#blah">Other with hash</a></li>
 </ul>
+
+<button onclick=@{ uriHelper.NavigateTo("RouterTest/Other"); }>
+    Programmatic navigation
+</button>