diff --git a/url/src/lib.rs b/url/src/lib.rs
index 1959a1213..a42c7f83a 100644
--- a/url/src/lib.rs
+++ b/url/src/lib.rs
@@ -2637,6 +2637,40 @@ impl Url {
         Err(())
     }
 
+    /// Append path segments to the path of a Url, escaping if necesary.
+    ///
+    /// This differs from Url::join in that it is insensitive to trailing slashes
+    /// in the url and leading slashes in the passed string. See documentation of Url::join for discussion
+    /// of this subtlety. Also, this function cannot change any part of the Url other than the path.
+    ///
+    /// Examples:
+    ///
+    /// ```
+    /// # use url::Url;
+    /// let mut my_url = Url::parse("http://www.example.com/api/v1").unwrap();
+    /// my_url.append_path("system/status").unwrap();
+    /// assert_eq!(my_url.as_str(), "http://www.example.com/api/v1/system/status");
+    /// ```
+    ///
+    /// Fails if the Url is cannot-be-a-base.
+    #[allow(clippy::result_unit_err)]
+    #[inline]
+    pub fn append_path(&mut self, path: impl AsRef<str>) -> Result<(), ()> {
+        // This fails if self is cannot-be-a-base but succeeds otherwise.
+        let mut path_segments_mut = self.path_segments_mut()?;
+
+        // Remove the last segment if it is empty, this makes our code tolerate trailing `/`'s
+        path_segments_mut.pop_if_empty();
+
+        // Remove any leading `/` from the path we are appending, this makes our code tolerate leading `/`'s
+        let path = path.as_ref();
+        let path = path.strip_prefix('/').unwrap_or(path);
+        for segment in path.split('/') {
+            path_segments_mut.push(segment);
+        }
+        Ok(())
+    }
+
     // Private helper methods:
 
     #[inline]
diff --git a/url/tests/unit.rs b/url/tests/unit.rs
index afe842beb..4dc757db9 100644
--- a/url/tests/unit.rs
+++ b/url/tests/unit.rs
@@ -1316,3 +1316,42 @@ fn issue_864() {
     url.set_path("x");
     dbg!(&url);
 }
+
+#[test]
+/// append_path is an alternative to Url::join addressing issues described in
+/// https://github.com/servo/rust-url/issues/333
+fn test_append_path() {
+    // append_path behaves as expected when path is `/` regardless of trailing & leading slashes
+    let mut url = Url::parse("http://test.com").unwrap();
+    url.append_path("/a/b/c").unwrap();
+    assert_eq!(url.as_str(), "http://test.com/a/b/c");
+
+    let mut url = Url::parse("http://test.com").unwrap();
+    url.append_path("a/b/c").unwrap();
+    assert_eq!(url.as_str(), "http://test.com/a/b/c");
+
+    let mut url = Url::parse("http://test.com/").unwrap();
+    url.append_path("/a/b/c").unwrap();
+    assert_eq!(url.as_str(), "http://test.com/a/b/c");
+
+    let mut url = Url::parse("http://test.com/").unwrap();
+    url.append_path("a/b/c").unwrap();
+    assert_eq!(url.as_str(), "http://test.com/a/b/c");
+
+    // append_path behaves as expected when path is `/api/v1` regardless of trailing & leading slashes
+    let mut url = Url::parse("http://test.com/api/v1").unwrap();
+    url.append_path("/a/b/c").unwrap();
+    assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c");
+
+    let mut url = Url::parse("http://test.com/api/v1").unwrap();
+    url.append_path("a/b/c").unwrap();
+    assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c");
+
+    let mut url = Url::parse("http://test.com/api/v1/").unwrap();
+    url.append_path("/a/b/c").unwrap();
+    assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c");
+
+    let mut url = Url::parse("http://test.com/api/v1/").unwrap();
+    url.append_path("a/b/c").unwrap();
+    assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c");
+}